package mspbots.cw.client.core.data.tickets;

import cn.hutool.core.util.ReUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mspbots.cw.client.core.AbstractToolsUtil;
import mspbots.cw.client.core.app.SyncRequest;
import mspbots.cw.client.core.client.ClientProperties;
import mspbots.cw.client.core.data.BaseSyncService;
import mspbots.cw.common.*;
import mspbots.cw.common.callback.CallbackAction;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.UUID;

/**
 * mspbots.data.cw.core.tickets.TicketEntryService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/10/18
 */
@Log4j2
@Service
@RequiredArgsConstructor
public class TicketsServiceImpl extends AbstractToolsUtil implements BaseSyncService {

    public Mono<JsonNode> sync(Long tenantId, SyncRequest syncRequest) {
        return super.loadProperties(tenantId)
                .doOnNext(clientProperties -> this.manualSync(clientProperties, syncRequest).subscribe())
                .map(clientProperties -> this.objectMapper.createObjectNode()
                        .put("result", "Tickets sync success."));
    }

    public Mono<ResponseEntity<JsonNode>> find(Long tenantId, QueryParams queryParams) {
        ResultSync resultSync = ResultSync.withDefault("TICKETS-SEARCH");
        return this.loadProperties(tenantId)
                .flatMap(properties -> this.get(properties, queryParams))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync));
    }

    private Mono<ResponseEntity<JsonNode>> get(ClientProperties properties, QueryParams queryParams) {
        log.debug("Get service tickets {} params {}.", properties.getTenantId(), queryParams);
        return super.cwWebClient.getEntity("/service/tickets", queryParams)
                .contextWrite(ctx -> ctx.put("properties", properties));
    }

    public Mono<ResponseEntity<JsonNode>> findProject(Long tenantId, QueryParams queryParams) {
        ResultSync resultSync = ResultSync.withDefault("TICKETS-SEARCH");
        return this.loadProperties(tenantId)
                .flatMap(properties -> this.getProject(properties, queryParams))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync));
    }

    private Mono<ResponseEntity<JsonNode>> getProject(ClientProperties properties, QueryParams queryParams) {
        log.debug("Get project tickets {} params {}.", properties.getTenantId(), queryParams);
        return super.cwWebClient.getEntity("/project/tickets", queryParams)
                .contextWrite(ctx -> ctx.put("properties", properties));
    }

    public Mono<ResponseEntity<JsonNode>> count(Long tenantId, QueryParams queryParams) {
        ResultSync resultSync = ResultSync.withDefault("TICKETS-COUNT");
        return super.loadProperties(tenantId)
                .flatMap(clientProperties -> this.count(clientProperties, queryParams))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync));
    }

    private Mono<ResponseEntity<JsonNode>> count(ClientProperties properties, QueryParams queryParams) {
        return super.cwWebClient.getEntity("/service/tickets/count", queryParams)
                .map(responseEntity -> Objects.requireNonNull(responseEntity.getBody()).get("count").longValue())
                .mergeWith(super.cwWebClient.getEntity("/project/tickets/count", queryParams)
                        .map(responseEntity -> Objects.requireNonNull(responseEntity.getBody()).get("count").longValue()))
                .collectList()
                .map(list -> (JsonNode) this.objectMapper.createObjectNode().put("count", list.parallelStream().mapToLong(r -> r).sum()))
                .map(ResponseEntity::ok)
                .contextWrite(ctx -> ctx.put("properties", properties));
    }

    public Flux<JsonNode> getAuditTrail(Long tenantId, Long ticketId) {
        ResultSync resultSync = ResultSync.withDefault("TICKETS-SEARCH");
        return this.loadProperties(tenantId)
                .flatMapMany(properties -> this.getAuditTrail(ticketId)
                        .contextWrite(ctx -> ctx.put("properties", properties)))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync));
    }

    public Mono<ObjectNode> buildTicket(JsonNode resultNode) {
        ObjectNode ticketNode = resultNode.deepCopy();
        return Mono.just(ticketNode)
                .flatMap(this::setCompanyTeamRoles)
                .flatMap(this::setStatusLastTime)
                .flatMap(this::setNotesResolutionFlag)
                .flatMap(this::setTimeEntryResolutionFlag)
                .flatMap(this::setConfigurations)
                .flatMap(this::setTicketTask);
    }

    @Override
    public Mono<Void> manualSync(ClientProperties properties, SyncRequest syncRequest) {
        ResultSync resultSync = ResultSync.withDefault("MANUAL-TICKETS-SYNC");
        String conditionsStr = "lastUpdated>=[" + syncRequest.getBeginTime().format(DateTimeFormatter.ISO_DATE_TIME) +
                "] and lastUpdated <=[" + syncRequest.getEndTime().format(DateTimeFormatter.ISO_DATE_TIME) + "]";
        return this.fetch(properties, QueryParams.withDefault().setConditions(conditionsStr)
                .setOrderBy("lastUpdated asc"))
                .flatMap(this::buildTicket)
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .doOnNext(ticketNode -> this.syncResult(properties.getTenantId(),
                        resultSync.getRequestId(), ticketNode))
                .then();
    }

    @Override
    public Mono<Void> syncFast(ClientProperties properties) {
        ResultSync resultSync = ResultSync.withDefault("TICKETS-SYNC");
        return this.filterCacheGet(properties)
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .doOnNext(companyNode -> this.syncResult(properties.getTenantId(),
                        resultSync.getRequestId(), companyNode))
                .doOnNext(ticketNode -> this.recordAnchorTime(RedisKeys.TICKETS_ANCHOR_REDIS_KEY_PREFIX + properties.getTenantId(),
                        ticketNode.findValue("lastUpdated").textValue()))
                .then();
    }

    public Flux<JsonNode> getNotes(Long tenantId, Long ticketId) {
        return this.loadProperties(tenantId)
                .flatMapMany(configuration -> super.cwWebClient.fetch("/service/tickets/" + ticketId + "/notes",
                        QueryParams.withDefault())
                        .contextWrite(ctx -> ctx.put("properties", configuration)));
    }

    private void syncResult(Long tenantId, UUID id, JsonNode resultNode) {
        this.sendEntity(RabbitKeys.TICKETS_SYNC_QUEUE_KEY, ResultMsg.builder()
                .requestId(id).id(resultNode.get("id").longValue())
                .action(CallbackAction.none.name()).entity(resultNode).tenantId(tenantId)
                .syncDate(LocalDateTime.now()).build());
    }

    private Flux<ObjectNode> filterCacheGet(ClientProperties properties) {
        return this.readAnchorTime(RedisKeys.TICKETS_ANCHOR_REDIS_KEY_PREFIX + properties.getTenantId())
                .flatMapMany(syncTime -> this.fetch(properties, QueryParams.withDefault()
                        .setConditions("lastUpdated>[" + syncTime.format(DateTimeFormatter.ISO_DATE_TIME) + "]")
                        .setOrderBy("lastUpdated asc")))
                .flatMap(this::buildTicket)
                .contextWrite(ctx -> ctx.put("properties", properties));
    }

    private Mono<ObjectNode> setConfigurations(ObjectNode ticketNode) {
        ticketNode.put("configurations", false);
        ResultSync resultSync = ResultSync.withDefault("TICKET-CONFIGURATIONS-SYNC");
        return this.cwWebClient.fetch("/service/tickets/" + ticketNode.get("id").longValue() + "/configurations",
                QueryParams.withDefault().setFields("id"))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .count()
                .onErrorReturn(0L)
                .defaultIfEmpty(0L)
                .map(resultCount -> {
                    ticketNode.put("configurations", resultCount > 0);
                    return ticketNode;
                });
    }

    private Mono<ObjectNode> setTicketTask(ObjectNode ticketNode) {
        String path = "service/tickets/";
        if (!ObjectUtils.isEmpty(ticketNode) && !ObjectUtils.isEmpty(ticketNode.get("project"))) {
            path = "project/tickets/";
        }
        ResultSync resultSync = ResultSync.withDefault("TICKET-TASK-SYNC");
        return this.cwWebClient.fetch(path + ticketNode.get("id").longValue() + "/tasks", QueryParams.withDefault())
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .collectList()
                .map(taskList -> {
                    if (taskList.size() > 0) {
                        ticketNode.putPOJO("tasks", taskList);
                    }
                    return ticketNode;
                }).onErrorReturn(ticketNode).defaultIfEmpty(ticketNode);
    }

    private Mono<ObjectNode> setTimeEntryResolutionFlag(ObjectNode ticketNode) {
        ticketNode.put("timeentryResolutionFlag", false);
        ResultSync resultSync = ResultSync.withDefault("TICKET-TIME-ENTRY-RESOLUTION-FLAG-SYNC");
        return super.cwWebClient.get("time/entries/count", QueryParams.withDefault()
                .setConditions("chargeToId=" + ticketNode.get("id").longValue() + " and addToResolutionFlag=true"))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .map(resultCountNode -> {
                    ticketNode.put("timeentryResolutionFlag", resultCountNode.get("count").intValue() > 0);
                    return ticketNode;
                }).onErrorReturn(ticketNode).defaultIfEmpty(ticketNode);
    }

    private Mono<ObjectNode> setNotesResolutionFlag(ObjectNode ticketNode) {
        ResultSync resultSync = ResultSync.withDefault("TICKET-NOTES-SYNC");
        return super.cwWebClient.get("service/tickets/" + ticketNode.get("id").longValue() + "/notes",
                QueryParams.withDefault().setConditions("resolutionFlag=true").setFields("id,resolutionFlag"))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .map(jsonNode -> {
                    if (jsonNode.size() > 0) {
                        ticketNode.put("resolutionFlag", true);
                    }
                    return ticketNode;
                }).onErrorReturn(ticketNode).defaultIfEmpty(ticketNode);
    }

    private Mono<ObjectNode> setStatusLastTime(ObjectNode ticketNode) {
        String regex = "(?<=\\bto\\b\\s?\")(?<=\").*?(?=\")";
        JsonNode statusNode = ticketNode.get("status");
        ResultSync resultSync = ResultSync.withDefault("TICKET-AUDIT-TRAIL-SYNC");
        return this.getAuditTrail(ticketNode.get("id").longValue())
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .filter(jsonNode -> !ObjectUtils.isEmpty(jsonNode.get("auditSubType")))
                .filter(jsonNode -> {
                    String textStr = ReUtil.get(regex, jsonNode.get("text").textValue(), 0);
                    return "Status".equals(jsonNode.get("auditSubType").textValue()) && textStr.equals(statusNode.get("name").textValue());
                })
                .map(jsonNode -> LocalDateTime.parse(jsonNode.findValue("enteredDate").asText(),
                        DateTimeFormatter.ISO_DATE_TIME))
                .last(LocalDateTime.parse(ticketNode.findValue("lastUpdated").textValue(),
                        DateTimeFormatter.ISO_ZONED_DATE_TIME))
                .map(statusTime -> {
                    ObjectNode audiNode = statusNode.deepCopy();
                    audiNode.putPOJO("time", statusTime);
                    ticketNode.putPOJO("auditor", audiNode);
                    return ticketNode;
                }).onErrorReturn(ticketNode).defaultIfEmpty(ticketNode);
    }

    private Mono<ObjectNode> setCompanyTeamRoles(ObjectNode ticketNode) {
        ResultSync resultSync = ResultSync.withDefault("TICKET-COMPANY-TEAM-ROLES-SYNC");
        return this.cwWebClient.fetch("/company/companies/" + ticketNode.get("company").get("id").longValue() + "/teams",
                QueryParams.withDefault())
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .collectList()
                .map(companyNodeList -> {
                    if (companyNodeList.size() > 0) {
                        ticketNode.putPOJO("teamRoles", companyNodeList);
                    }
                    return ticketNode;
                }).onErrorReturn(ticketNode).defaultIfEmpty(ticketNode);
    }

    private Flux<JsonNode> getAuditTrail(Long ticketId) {
        QueryParams queryParams = QueryParams.withDefault();
        queryParams.set("type", "Ticket");
        queryParams.set("id", ticketId.toString());
        return super.cwWebClient.fetch("/system/audittrail", queryParams);
    }

    private Flux<JsonNode> fetch(ClientProperties properties, QueryParams queryParams) {
        log.debug("Get tickets {} params {}.", properties.getTenantId(), queryParams);
        return super.cwWebClient.fetch("/service/tickets", queryParams)
                .mergeWith(super.cwWebClient.fetch("/project/tickets", queryParams)
                        .onErrorResume(err -> Mono.empty()))
                .contextWrite(ctx -> ctx.put("properties", properties));
    }


}
